package com.mxx.common.data.jpa.auditing;

import com.mxx.common.data.jpa.Spec;
import com.mxx.common.data.jpa.UpdateSpec;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.data.auditing.AuditingHandler;
import org.springframework.data.auditing.CurrentDateTimeProvider;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;

import java.time.temporal.TemporalAccessor;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

@ConditionalOnBean(AuditingHandler.class)
public class AuditingSpecInterceptor implements SpecInterceptor {

    private final Map<Class<?>, MappingAuditingMetadata> metadataCache;
    private final JpaMetamodelMappingContext mappingContext;
    private final Optional<AuditorAware> auditorAware;

    public AuditingSpecInterceptor(JpaMetamodelMappingContext mappingContext, Optional<AuditorAware> auditorAware) {
        this.metadataCache = new ConcurrentHashMap<>();
        this.mappingContext = mappingContext;
        this.auditorAware = auditorAware;
    }

    @Override
    public void select(SpecState state) {
        MappingAuditingMetadata auditingMetadata = this.getAuditingMetadata(state.getDomainClass());
        if (auditingMetadata.isAuditable()) {
            if (DeleteFlagThreadLocal.isUse()) {
                Spec spec = helper -> {
                    auditingMetadata.deleteFlagPaths.forEach(p -> {
                        Class deleteFlagType = p.getBaseProperty().getType();
                        if (Boolean.class.isAssignableFrom(deleteFlagType)) {
                            helper.equal(p.getBaseProperty().getName(), 0);
                        } else if (Number.class.isAssignableFrom(deleteFlagType)) {
                            helper.isFalse(p.getBaseProperty().getName());
                        }
                    });
                    return helper.and().endAnd();
                };
                state.flush(state.getSpec().and(spec));
            }
        }
    }

    @Override
    public void update(SpecState state) {
        MappingAuditingMetadata auditingMetadata = this.getAuditingMetadata(state.getDomainClass());
        if (auditingMetadata.isAuditable()) {
            TemporalAccessor temporalAccessor = CurrentDateTimeProvider.INSTANCE.getNow().orElse(null);
            Object currentAuditor = auditorAware.map(p -> p.getCurrentAuditor()).orElse(Optional.empty()).orElse(null);

            UpdateSpec spec = helper -> {
                if (state.getOriginalType() == SpecType.UPDATE) {
                    auditingMetadata.lastModifiedByPaths.forEach(p -> helper.set(p.getBaseProperty().getName(), currentAuditor));
                    auditingMetadata.lastModifiedDatePaths.forEach(p -> helper.set(p.getBaseProperty().getName(), temporalAccessor));
                }
                auditingMetadata.versionPaths.forEach(p -> helper.sum(p.getBaseProperty().getName(), 1));
                auditingMetadata.javaxVersionPaths.forEach(p -> helper.sum(p.getBaseProperty().getName(), 1));
                auditingMetadata.deleteFlagPaths.forEach(p -> {
                    if (DeleteFlagThreadLocal.isUse()) {
                        Class deleteFlagType = p.getBaseProperty().getType();
                        if (Boolean.class.isAssignableFrom(deleteFlagType)) {
                            helper.equal(p.getBaseProperty().getName(), 0);
                        } else if (Number.class.isAssignableFrom(deleteFlagType)) {
                            helper.isFalse(p.getBaseProperty().getName());
                        }
                    }
                });

                return helper.updateEndAnd();
            };
            state.flush(state.getSpec().and(spec));
        }
    }

    @Override
    public void delete(SpecState state) {
        MappingAuditingMetadata auditingMetadata = this.getAuditingMetadata(state.getDomainClass());
        if (auditingMetadata.isAuditable()) {
            AtomicBoolean toUpdate = new AtomicBoolean(false);
            UpdateSpec spec = helper -> {
                TemporalAccessor temporalAccessor = CurrentDateTimeProvider.INSTANCE.getNow().orElse(null);
                Object currentAuditor = auditorAware.map(p -> p.getCurrentAuditor()).orElse(Optional.empty()).orElse(null);
                auditingMetadata.deleteByPaths.forEach(p -> helper.set(p.getBaseProperty().getName(), currentAuditor));
                auditingMetadata.deleteTimePaths.forEach(p -> helper.set(p.getBaseProperty().getName(), temporalAccessor));
                auditingMetadata.deleteFlagPaths.forEach(p -> {
                    if (DeleteFlagThreadLocal.isUse()) {
                        Class deleteFlagType = p.getBaseProperty().getType();
                        if (Boolean.class.isAssignableFrom(deleteFlagType)) {
                            helper.set(p.getBaseProperty().getName(), 1);
                        } else if (Number.class.isAssignableFrom(deleteFlagType)) {
                            helper.set(p.getBaseProperty().getName(), true);
                        }
                    }
                    toUpdate.set(true);
                });
                return helper.updateEndAnd();
            };
            if (toUpdate.get()) {
                state.toUpdate(spec.and(state.getSpec()));
            } else {
                state.flush(state.getSpec().and(spec));
            }
        }
    }

    private MappingAuditingMetadata getAuditingMetadata(Class classes) {
        return metadataCache.computeIfAbsent(classes, t -> new MappingAuditingMetadata(mappingContext, t));
    }

    @Override
    public int sort() {
        return 1;
    }
}
